/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package cz.mzk.oai;

import ORG.oclc.oai.server.catalog.AbstractCatalog;
import ORG.oclc.oai.server.crosswalk.Crosswalk;
import ORG.oclc.oai.server.crosswalk.CrosswalkItem;
import ORG.oclc.oai.server.verb.BadArgumentException;
import ORG.oclc.oai.server.verb.BadResumptionTokenException;
import ORG.oclc.oai.server.verb.CannotDisseminateFormatException;
import ORG.oclc.oai.server.verb.IdDoesNotExistException;
import ORG.oclc.oai.server.verb.NoItemsMatchException;
import ORG.oclc.oai.server.verb.NoMetadataFormatsException;
import ORG.oclc.oai.server.verb.NoSetHierarchyException;
import ORG.oclc.oai.server.verb.OAIInternalServerError;
import cz.mzk.oai.metadataRepository.metadata.DateStamp;
import cz.mzk.oai.metadataRepository.metadata.MetadataFormat;
import cz.mzk.oai.metadataRepository.repository.OaiSet;
import cz.mzk.oai.metadataRepository.repository.collections.Item;
import cz.mzk.oai.metadataRepository.repository.collections.MetadataCollection;
import cz.mzk.oai.metadataRepository.repository.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentFactory;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.QName;

/**
 *
 * @author Martin Rehanek, Martin.Rehanek@gmail.com
 */
public class Catalog extends AbstractCatalog {

    private Repository repository;
    private int maxListSize;
    private DateStamp earliestDatestamp;
    private static String RESUMPTION_TOKEN_SEPARATOR = "/";
    private Map<String, Character> codeMap;

    public Catalog(Properties properties) {
        maxListSize = Integer.parseInt(properties.getProperty("Catalog.maxListSize"));
        earliestDatestamp = new DateStamp(properties.getProperty("Identify.earliestDatestamp"));
        repository = Repository.instanceOf(properties);
        initCodeMap();
    }

    private void initCodeMap() {
        codeMap = new HashMap<String, Character>();
        codeMap.put("20", ' ');
        codeMap.put("21", '!');
        codeMap.put("22", '"');
        codeMap.put("23", '#');
        codeMap.put("24", '$');
        codeMap.put("25", '%');
        codeMap.put("26", '&');
        codeMap.put("27", '\'');
        codeMap.put("28", '(');
        codeMap.put("29", ')');
        codeMap.put("2A", '*');
        codeMap.put("2B", '+');
        codeMap.put("2C", ',');
        codeMap.put("2D", '-');
        codeMap.put("2E", '.');
        codeMap.put("2F", '/');

        codeMap.put("3A", ':');
        codeMap.put("3B", ';');
        codeMap.put("3C", '<');
        codeMap.put("3D", '=');
        codeMap.put("3E", '>');
        codeMap.put("3F", '?');

        codeMap.put("40", '@');

        codeMap.put("5B", '[');
        codeMap.put("5C", '\\');
        codeMap.put("5D", ']');
        codeMap.put("5E", '^');
        codeMap.put("5F", '_');

        codeMap.put("7E", '~');
        codeMap.put("7B", '{');
        codeMap.put("7C", '|');
        codeMap.put("7D", '}');
    }

    @Override
    public Map listSets() throws NoSetHierarchyException, OAIInternalServerError {
        Map listSetsMap = new LinkedHashMap();
        listSetsMap.put("sets", getSetsXmls().iterator());
        return listSetsMap;
    }

    /**
     * @return
     */
    public List<String> getSetsXmls() {
        List<String> setXmls = new ArrayList<String>();
        Set<OaiSet> sets = repository.getSets();
        for (OaiSet set : sets) {
            Document document = DocumentHelper.createDocument();
            Element setEl = document.addElement("set");
            Element setSpec = setEl.addElement("setSpec");
            setSpec.addText(set.getSetSpec());
            Element setName = setEl.addElement("setName");
            setName.addText(set.getSetName());
            Element setDesc = setEl.addElement("setDescription");
            Namespace oai_dc = new Namespace(MetadataFormat.oai_dc.name(), MetadataFormat.oai_dc.getNamespace());
            Namespace xsi = new Namespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
            Namespace dc = new Namespace("dc", "http://purl.org/dc/elements/1.1/");
            Element dcEl = setDesc.addElement(new QName("dc", oai_dc));
            DocumentFactory factory = DocumentFactory.getInstance();
            Attribute schemaLocation = factory.createAttribute(dcEl, new QName("schemaLocation", xsi), "http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd");
            dcEl.add(schemaLocation);
            Element dcDescEl = dcEl.addElement(new QName("description", dc));
            dcDescEl.addText("This set contains " + set.getSize() + " items");
            setXmls.add(document.getRootElement().asXML());
            //System.out.println("node: " + document.getRootElement().asXML());
        }
        return setXmls;
    }

    @Override
    public Map listSets(String resumptionToken) throws BadResumptionTokenException, OAIInternalServerError {
        throw new BadResumptionTokenException();
    }

    @Override
    public Vector getSchemaLocations(String identifier) throws IdDoesNotExistException, NoMetadataFormatsException, OAIInternalServerError {
        String decodedId = decode(identifier);
        Item item = repository.getItem(decode(decodedId));
        if (item == null) {
            throw new IdDoesNotExistException(decodedId);
        }
        return getRecordFactory().getSchemaLocations(item);
    }

    @Override
    public Map listIdentifiers(String from, String until, String set, String metadataPrefix) throws BadArgumentException, CannotDisseminateFormatException, NoItemsMatchException, NoSetHierarchyException, OAIInternalServerError {
        //from
        DateStamp fromDs;
        if (from == null || from.equals("")
                || from.equals("0001-01-01T00:00:00Z")) {
            //oaicat returns "0001-01-01T00:00:00Z" in from if from parametr is not present in URL
            //it should return "" or null, because this way I had to solve it incorrectly 
            //if there was actualy from=0001-01-01T00:00:00Z than this would act as if from was not specified
            //and instead of BadArgumentException it would proceed
            //FIXME
            fromDs = earliestDatestamp;
        } else {
            fromDs = new DateStamp(from);
            if (fromDs.compareTo(earliestDatestamp) < 0) {
                throw new NoItemsMatchException();
            }
        }
        //until
        DateStamp untilDs;
        if (until == null || until.equals("") || until.equals("9999-12-31T23:59:59Z")) {
            untilDs = new DateStamp("9999-12-31T23:59:59Z");
        } else {
            untilDs = new DateStamp(until);
            if (untilDs.compareTo(earliestDatestamp) < 0) {
                throw new NoItemsMatchException();
            }
        }
        //from&until
        if (fromDs.compareTo(untilDs) > 0) {
            throw new BadArgumentException();
        }
        //set
        if ("null".equals(set)) {
            set = null;
        }
        Set<MetadataCollection> cols = repository.collectionsInSet(set);
        if (cols == null) {
            throw new NoItemsMatchException();
        }
        List<Item> matchingItems = getItems(metadataPrefix, cols, from, until, set);
        if (matchingItems.isEmpty()) {
            throw new NoItemsMatchException();
        }
        int fromCounter = 0;
        int toCounter = Math.min(maxListSize, matchingItems.size());
        return listIdentifiers(matchingItems, fromCounter, toCounter, metadataPrefix, from, until, set);
    }

    @Override
    public Map listIdentifiers(String resumptionToken) throws BadResumptionTokenException, OAIInternalServerError {
        String resumptionTokenDecoded = decode(resumptionToken);
        String tokens[] = resumptionTokenDecoded.split(RESUMPTION_TOKEN_SEPARATOR);
        int fromCounter;
        int toCounter;
        int completeSize;
        String metadataPrefix;
        String set;
        String from;
        String until;
        try {
            fromCounter = Integer.parseInt(tokens[0]);
            toCounter = Integer.parseInt(tokens[1]);
            completeSize = Integer.parseInt(tokens[2]);//TODO: je mi to tady na neco?
            metadataPrefix = tokens[3];
            set = "null".equals(tokens[4]) ? null : tokens[4];
            from = tokens[5];
            until = tokens[6];
        } catch (Exception e) {
            throw new BadResumptionTokenException();
        }
        checkCounters(fromCounter, toCounter, completeSize);
        int newFromCounter = toCounter;
        int newToCounter = Math.min(toCounter + maxListSize, completeSize);

        Set<MetadataCollection> cols = repository.collectionsInSet(set);
        if (cols == null) {
            throw new BadResumptionTokenException();
        }
        List<Item> matchingItems = getItems(metadataPrefix, cols, from, until, set);
        try {
            return listIdentifiers(matchingItems, newFromCounter, newToCounter, metadataPrefix, from, until, set);
        } catch (IndexOutOfBoundsException e) {
            throw new BadResumptionTokenException();
        }
    }

    private Map listIdentifiers(List<Item> allMatchingItems, int fromCounter, int toCounter, String metadataPrefix, String from, String until, String set) {
        System.out.println("fromCounter: " + fromCounter);
        System.out.println("toCounter: " + toCounter);
        System.out.println("size: " + allMatchingItems.size());
        List<Item> sublist = allMatchingItems.subList(fromCounter, toCounter);
        List<String> headers = getHeaders(sublist);
        List<String> identifiers = getIdentifiers(sublist);
        Map listIdentifiersMap = new HashMap();
        listIdentifiersMap.put("headers", headers.iterator());
        listIdentifiersMap.put("identifiers", identifiers.iterator());
        if (toCounter < allMatchingItems.size()) {
            int completeSize = allMatchingItems.size();
            String resumptionToken = createResumptionToken(metadataPrefix, from, until, set, fromCounter, toCounter, completeSize);
            //System.out.println("Catalog.listIdentifiers.resumptionToken" + resumptionToken);
            listIdentifiersMap.put("resumptionMap", getResumptionMap(resumptionToken, completeSize, fromCounter));
            //System.out.println("Catalog.listIdentifiers.resumptionMap" + getResumptionMap(resumptionToken, completeSize, newFromCounter));
            listIdentifiersMap.put("metadataPrefix", metadataPrefix);
        }
        return listIdentifiersMap;
    }

    /**
     * Retrieve a list of records that satisfy the specified criteria
     *
     * @param from beginning date in the form of YYYY-MM-DD or null if earliest
     * date is desired
     * @param until ending date in the form of YYYY-MM-DD or null if latest
     * date is desired
     * @param set set name or null if no set is desired
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception BadArgumentException one or more of the arguments are bad.
     * @exception CannotDisseminateFormatException the requested metadataPrefix isn't supported
     * @exception NoItemsMatchException no items fit the criteria
     * @exception NoSetHierarchyException sets aren't defined for this repository
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    @Override
    public Map listRecords(String from, String until, String set, String metadataPrefix)
            throws BadArgumentException, CannotDisseminateFormatException, NoItemsMatchException,
            NoSetHierarchyException, OAIInternalServerError {
        Map identifiersMap = listIdentifiers(from, until, set, metadataPrefix);
        return listRecords(identifiersMap, metadataPrefix);
    }

    /**
     * Retrieve the next set of records associated with the resumptionToken
     *
     * @param resumptionToken implementation-dependent format taken from the
     * previous listRecords() Map result.
     * @return a Map object containing an optional "resumptionToken" key/value
     * pair and a "records" Iterator object. The "records" Iterator contains a
     * set of Records objects.
     * @exception BadResumptionTokenException The resumptionToken is bad.
     * @exception OAIInternalServerError signals an http status code 500 problem
     */
    @Override
    public Map listRecords(String resumptionToken)
            throws BadResumptionTokenException, OAIInternalServerError {
        String resumptionTokenDecoded = decode(resumptionToken);
        String tokens[] = resumptionTokenDecoded.split(RESUMPTION_TOKEN_SEPARATOR);
        int fromCounter;
        int toCounter;
        int completeSize;
        String metadataPrefix;
        String set;
        String from;
        String until;
        try {
            fromCounter = Integer.parseInt(tokens[0]);
            toCounter = Integer.parseInt(tokens[1]);
            completeSize = Integer.parseInt(tokens[2]);//TODO: je mi to tady na neco?
            metadataPrefix = tokens[3];
            set = "null".equals(tokens[4]) ? null : tokens[4];
            from = tokens[5];
            until = tokens[6];
        } catch (Exception e) {
            throw new BadResumptionTokenException();
        }
        checkCounters(fromCounter, toCounter, completeSize);

        int newFromCounter = toCounter;
        int newToCounter = Math.min(toCounter + maxListSize, completeSize);

        Set<MetadataCollection> cols = repository.collectionsInSet(set);
        if (cols == null) {
            throw new BadResumptionTokenException();
        }
        List<Item> matchingItems = getItems(metadataPrefix, cols, from, until, set);
        try {
            Map identifiersMap = listIdentifiers(matchingItems, newFromCounter, newToCounter, metadataPrefix, from, until, set);
            return listRecords(identifiersMap, metadataPrefix);
        } catch (IndexOutOfBoundsException e) {
            throw new BadResumptionTokenException();
        } catch (CannotDisseminateFormatException e) {
            // someone cheated
            throw new BadResumptionTokenException();
        }
    }

    private Map listRecords(Map identifiersMap, String metadataPrefix) throws CannotDisseminateFormatException, OAIInternalServerError {
        Map resumptionMap = (Map) identifiersMap.get("resumptionMap");
        Iterator identifiers = (Iterator) identifiersMap.get("identifiers");

        Map listRecordsMap = new HashMap();
        ArrayList records = new ArrayList();
        while (identifiers.hasNext()) {
            String identifier = (String) identifiers.next();
            try {
                records.add(getRecord(identifier, metadataPrefix));
            } catch (IdDoesNotExistException e) {
                throw new OAIInternalServerError("GetRecord failed to retrieve identifier '"
                        + identifier + "'");
            }
        }
        listRecordsMap.put("records", records.iterator());
        if (resumptionMap != null) {
            listRecordsMap.put("resumptionMap", resumptionMap);
        }
        return listRecordsMap;
    }

    private String createResumptionToken(String metadataPrefix, String from, String until, String set, int fromCounter, int toCounter, int completeSize) {
        StringBuilder builder = new StringBuilder();
        builder.append(fromCounter);
        builder.append(RESUMPTION_TOKEN_SEPARATOR);
        builder.append(toCounter);
        builder.append(RESUMPTION_TOKEN_SEPARATOR);
        builder.append(completeSize);
        builder.append(RESUMPTION_TOKEN_SEPARATOR);
        builder.append(metadataPrefix);
        builder.append(RESUMPTION_TOKEN_SEPARATOR);
        builder.append(set);
        builder.append(RESUMPTION_TOKEN_SEPARATOR);
        builder.append(from);
        builder.append(RESUMPTION_TOKEN_SEPARATOR);
        builder.append(until);
        return builder.toString();
    }

    private List<Item> getItems(String metadataPrefix, Set<MetadataCollection> collections, String from, String until, String set) {
        List<Item> items = new ArrayList<Item>();
        List<Item> itemsPart;
        for (MetadataCollection col : collections) {
            itemsPart = getItems(metadataPrefix, col, from, until, set);
            if (itemsPart != null) {
                items.addAll(itemsPart);
            }
        }
        return items;
    }

    private List<Item> getItems(String metadataPrefix, MetadataCollection col, String from, String until, String requestedSet) {
        List<Item> allItems = col.getItems(MetadataFormat.valueOf(metadataPrefix));
        List<Item> matchItems = new ArrayList<Item>();
        Set<OaiSet> sets = col.getOaiSets();
        boolean collectionInSet = false;
        if (requestedSet != null) {//selective harvesting according to set
            for (OaiSet newSet : sets) {
                if (newSet.getSetSpec().equals(requestedSet)) {
                    collectionInSet = true;
                    break;
                }
            }
            if (!collectionInSet) {
                //System.out.println("collection " + col.getName() + " not in set " + set);
                return null;
            }
        }
        for (Item item : allItems) {
            if (isBetween(item.getDateStamp(), from, until)) {
                matchItems.add(item);
            }
        }
        return matchItems;
    }

    /*
     * Returns true if dateTime is between from and until (inclusive) ie: <from, until>
     */
    private boolean isBetween(DateStamp dateTime, String from, String until) {
        DateStamp fromDS = new DateStamp(from);
        DateStamp untilDS = new DateStamp(until);
        boolean afterFrom = (dateTime.compareTo(fromDS) == 0) || (dateTime.compareTo(fromDS) == 1);
        boolean beforeUntil = (dateTime.compareTo(untilDS) == 0) || (dateTime.compareTo(untilDS) == -1);
        return afterFrom && beforeUntil;
    }

    private void checkCounters(int fromCounter, int toCounter, int completeSize) throws BadResumptionTokenException {
        if (fromCounter < 0 || toCounter < 0 || completeSize < 0
                || (toCounter != fromCounter + maxListSize)
                || (toCounter > completeSize)) {
            throw new BadResumptionTokenException();
        }
    }

    @Override
    public String getRecord(String identifier, String metadataPrefix) throws IdDoesNotExistException, CannotDisseminateFormatException, OAIInternalServerError {
        Iterator iterator = getCrosswalks().iterator();
        //String result = "required prefix: "  + metadataPrefix;
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry) iterator.next();
            String itemPrefix = (String) entry.getKey();
            CrosswalkItem crosswalkItem = (CrosswalkItem) entry.getValue();
            Crosswalk crosswalk = crosswalkItem.getCrosswalk();
            if (itemPrefix.equals(metadataPrefix)) {
                String decodedId = decode(identifier);
                Item item = repository.getItem(decodedId);
                if (item == null) {
                    throw new IdDoesNotExistException(decodedId);
                }
                StringBuilder builder = new StringBuilder();
                builder.append("<record>");
                builder.append(this.getRecordFactory().createHeader(item)[0]);
                builder.append("<metadata>");
                builder.append(crosswalk.createMetadata(item));
                builder.append("</metadata>");
                builder.append("</record>");
                return builder.toString();
            }
        }
        throw new CannotDisseminateFormatException(metadataPrefix);
    }

    @Override
    public void close() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    private List<String> getIdentifiers(List<Item> items) {
        List<String> result = new ArrayList<String>(items.size());
        for (Item item : items) {
            result.add(item.getId());
        }
        return result;
    }

    private List<String> getHeaders(List<Item> items) {
        List<String> result = new ArrayList<String>(items.size());
        for (Item item : items) {
            result.add(getRecordFactory().createHeader(item)[0]);
        }
        return result;
    }

    /**
     *
     * @param encoded string in form oai%3Aaleph%2Emzk%2Ecz%3AMZK03-001042910.
     * (Could also be resumptionToken or anything encoded)
     * @return string in form oai:aleph.mzk.cz:MZK03-001042910
     */
    private String decode(String encoded) {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < encoded.length(); i++) {
            char character = encoded.charAt(i);
            if (character == '%') {
                String code = encoded.substring(i + 1, i + 3);
                Character decoded = codeMap.get(code.toUpperCase());
                if (decoded == null) {
                    return null;
                }
                result.append(decoded);
                i += 2;
            } else {
                result.append(character);
            }
        }
        return result.toString();
    }
}
